/**
 * @authors Aaron Hong, Sowmya Balasubramanian, David Robertson
 */
package net.es.weathermap {

    import com.google.maps.Map;
    import com.google.maps.MapZoomEvent;
    import com.google.maps.MapMouseEvent;
    import com.google.maps.MapMoveEvent;
    import com.google.maps.MapEvent;
    import com.google.maps.LatLng;
    import com.google.maps.LatLngBounds;
    import com.google.maps.MapType;
    import com.google.maps.overlays.*;
    import com.google.maps.interfaces.IProjection;
    import flash.display.Loader;
    import flash.net.URLRequest;
    import flash.geom.*
    import flash.utils.clearInterval;
    import flash.utils.setInterval;
    import flash.external.ExternalInterface;
    import mx.controls.*;
    import mx.controls.dataGridClasses.*;
    import mx.collections.*;
    import com.google.maps.InfoWindowOptions;
    import com.google.maps.Copyright;
    import flash.events.Event;
    import com.google.maps.geom.*;

    public class Renderer {
        private var map:Map;
        private var markersOnMap:Array;
        private var linksOnMap:Array;
        private var arrowsOnMap:Array;
        private var prevColorsOnMap:Array;
        private var pathToAdd:Polyline;
        private var pathToRemove:Polyline;
        private var pathOnMap:Polyline;
        private var pathName:String;
        private var typeOfRouter:Array;
        private var markerLogo:Marker;
        private var markerLocation:Array;
        private var bg:GroundOverlay;
        private var infoWindowLabel:String;
        private var infoWindowClosed:Boolean;
        private var demoMode:String;

        public function Renderer(map:Map) {
            this.map = map;
            markersOnMap = new Array();
            linksOnMap = new Array();
            arrowsOnMap = new Array();
            prevColorsOnMap = new Array();
            typeOfRouter = new Array();
            markerLocation = new Array();
            demoMode = "";
            infoWindowClosed = true;
            map.addEventListener(MapZoomEvent.ZOOM_CHANGED, onMapZoomChanged);
            map.addEventListener(MapMoveEvent.MOVE_STEP, onMapPanEnd);
            map.addEventListener(MapEvent.INFOWINDOW_OPENED, onInfoWindowOpened);
            map.addEventListener(MapEvent.INFOWINDOW_CLOSED, onInfoWindowClosed);
        }

        private function loadIcon(url:String):Loader {
            var loader:Loader = new Loader();
            var request:URLRequest = new URLRequest(url);
            loader.load(request);
            return loader;
        }

        public function onMapZoomChanged(evt:MapZoomEvent):void {
            //trace("inside mapzoom changed");
            var newLevel:Number = map.getZoom();
            //testing for getMarkersInViewPort function
            //trace("list of markers in viewport");
            var viewport:LatLngBounds = map.getLatLngBounds();

            //trace(markerList);
            /*
            if (newLevel < 6 ) {
                var markerList:Array = getMarkersInViewPort(viewport);
                for (var i:int=0; i<markerList.length; i++) {
                    for (var j:int=0; j<markerList.length; j++) {
                        markerLocation[markerList[i]].distanceFrom(markerLocation[markerList[j]]) / 1000;
                    }
                }
            }
            */

            var changed:Boolean = false;
            var lod:Number;
            if (newLevel < 9 && lod != 0) {
                lod = 0;
                changed = true;
            } else if(newLevel >= 9 && lod != 1) {
                lod = 1;
                changed = true;
            }
            if (changed==true) {
                triggerEventJS(lod);
            }
        }

        public function drawCopyrightInfo():void {
            // may be called on resize event when everything not set up yet
            if (!map) {
                return;
            }
            var newPoint:Point;
            var markerLabel:String = "(c) Copyright 2009";
            // if not special case
            if (!demoMode) {
                var viewport:LatLngBounds=map.getLatLngBounds();
                var northEast:LatLng = viewport.getNorthEast();
                var point:Point = map.fromLatLngToViewport(northEast,true);
                newPoint = new Point(point.x-150,point.y-5);
            } else if (demoMode == "globe") {
                var centerAtlantic:LatLng = new LatLng(40.0, -50.0);
                newPoint = map.fromLatLngToViewport(centerAtlantic,true);
            } else if (demoMode == "us") {
                var eh:LatLng = new LatLng(51.4, -91.0);
                newPoint = map.fromLatLngToViewport(eh,true);
            }
            if (markerLogo != null) {
                map.removeOverlay(markerLogo);
            }
            var markerCoord:LatLng = map.fromViewportToLatLng(newPoint);
            markerLogo = new Marker(markerCoord,
            new MarkerOptions({
                icon: new CustomIconSprite(markerLabel,"logo"),
                hasShadow:false
            }));
            map.addOverlay(markerLogo);
        }

        private function onMapPanEnd(evt:MapMoveEvent):void {
            drawCopyrightInfo();
        }

        private function onInfoWindowOpened(evt:MapMoveEvent):void {
            infoWindowClosed = false;
        }

        private function onInfoWindowClosed(evt:MapMoveEvent):void {
            infoWindowClosed = true;
        }

        private function triggerEventJS(lod:Number):void {
            ExternalInterface.call("triggerLODChange",lod);
        }

        /**
         * This function places the nodes on the map. An array of markers is passed as parameter.
         * (for now it takes the array of markers and plots. Can be modified as per requirement)
         */
        public function drawNodes():void {
            for (var nodeName:String in markersOnMap){
                map.addOverlay(markersOnMap[nodeName]);
            }
            var viewport:LatLngBounds=map.getLatLngBounds();

            // force the map to move so that markers are put in the right spot
            // this is a workaround for a google maps api bug
            map.panTo(new LatLng( map.getCenter().lat()+0.00000001, map.getCenter().lng()));
        }

        // only used for large screen monitor demo
        public function setDemo(demo:String):void {
            var pt:Point;
            var panTo:LatLng;
            demoMode = demo;
            if (demo == "globe") {
                pt = new Point(2048, 1024);
                map.setSize(pt);
                // scroll a little bit down and a bit to the right
                panTo = new LatLng(20.0, -89.0);
                map.setZoom(3);
                map.panTo(panTo);
            }
        }

        /**
         * This function draws the links on the map.
         */
        public function drawLinks():void {
            for (var linkName:String in linksOnMap){
                map.addOverlay(linksOnMap[linkName]);
                map.addOverlay(arrowsOnMap[linkName]);
            }
            map.panTo(new LatLng( map.getCenter().lat()+0.00000001, map.getCenter().lng()));
        }

        public function createMarker(point:LatLng, type:String, label:String,
                                     info:String):void {
            var markerOptions:MarkerOptions = new MarkerOptions();
            var table:Array;
            var icon:CustomIconSprite = new CustomIconSprite(label, type);
            var offset:Number = icon.getOffset();
            markerOptions.icon = icon;
            //markerOptions.tooltip = label;
            markerOptions.hasShadow=false;
            markerOptions.iconAlignment = MarkerOptions.ALIGN_HORIZONTAL_CENTER | MarkerOptions.ALIGN_VERTICAL_CENTER;
            // correction for bad alignment for long labels
            markerOptions.iconOffset = new Point(offset, 2);

            var marker:Marker = new Marker(point, markerOptions);
            markersOnMap[label] = marker;
            typeOfRouter[label] = type;
            markerLocation[label] = point;
            var intervalId:uint;
            var onSingleClick:Function = function():void {
                // if mouse click, bring up info window for node
                if (ExternalInterface.available) {
                    // ExternalInterface.call("console.log", "mouse click");
                    infoWindowLabel = label;
                    var networks:Array = new Array();
                    networks.push("SDN");
                    networks.push("IP");
                    networks.push("PEER");
                    table = ExternalInterface.call("getInfoCard", label, networks);
                }
                var options2:InfoWindowOptions = new InfoWindowOptions({
                     customContent: new InfoWindowTabbedComponent(table),
                     width: 690,
                     height: 250,
                     drawDefaultFrame: true
                });
                map.openInfoWindow(point, options2);
            };
            var onDoubleClick:Function = function():void {
                // (double clicking on map only zooms one level)
                // zooms in to level 10 if double click at lower resolution
                // zooms out to level 4 if at 10 or above
                if (map.getZoom() < 10) {
                    map.setCenter(point, 10, MapType.NORMAL_MAP_TYPE);
                } else {
                    map.setCenter(point, 4, MapType.NORMAL_MAP_TYPE);
                }
            };
            // double click handling adapted from
            // http://www.actionscript.org/resources/articles/566/1/Doubleclick-Event-for-MovieClip/Page1.html
            // If single click and double click handlers are defined, a double
            // click results in both being called without this
            var singleClickHandler:Function = function():void {
                if (intervalId) {
                    onSingleClick();
                    clearInterval(intervalId);
                    intervalId = 0;
                }
            };
            marker.addEventListener(MapMouseEvent.MOUSE_UP, function(e:MapMouseEvent):void {
                if (intervalId) {
                    onDoubleClick();
                    clearInterval(intervalId);
                    intervalId = 0;
                } else {
                    intervalId = setInterval(singleClickHandler, 200);
                }
            });
        }

        public function createPolyline(name:String, src:LatLng, dst:LatLng, index:int, capacity:Number):void {
            var lngOffset:Number = getLngOffset(src.lng(), dst.lng());
            var newSrc:LatLng = new LatLng(src.lat(), adjustLngTo(src.lng(), lngOffset));
            var newDst:LatLng = new LatLng(dst.lat(), adjustLngTo(dst.lng(), lngOffset));
            var curve1:Array = createMapCurve(newDst,newSrc,index,"first-half");
            var curve2:Array = createMapCurve(newSrc,newDst,index,"first-half");

            var lineIn:Polyline;
            var lineOut:Polyline;
            if (lngOffset == 0.0) {
                lineIn = new Polyline(curve2);
                lineOut = new Polyline(curve1);
            } else {
                var newCurve1:Array = new Array();
                for (var i:int = 0; i < curve1.length; i++) {
                    var newLng:Number = adjustLngFrom(curve1[i].lng(), lngOffset);
                    if (newLng > 0.0) {
                        newCurve1.push(new LatLng(curve1[i].lat(), newLng));
                    } else {
                        newCurve1.push(new LatLng(curve1[i].lat(), newLng+360.0, true));
                    }
                }
                var newCurve2:Array = new Array();
                for (var j:int = 0; j < curve2.length; j++) {
                    var newLng2:Number = adjustLngFrom(curve2[j].lng(), lngOffset);
                    if (newLng2 > 0.0) {
                        newCurve2.push(new LatLng(curve2[j].lat(), newLng2));
                    } else {
                        newCurve2.push(new LatLng(curve2[j].lat(), newLng2+360.0, true));
                    }
                }
                lineIn = new Polyline(newCurve2);
                lineOut = new Polyline(newCurve1);
            }
            var lineInLength:Number = lineIn.getVertexCount();
            var lineOutLength:Number = lineOut.getVertexCount();
            var point1:LatLng = lineOut.getVertex(Math.round(lineOutLength/2));
            var point2:LatLng = lineIn.getVertex(Math.round(lineInLength/2));

            var markerOptions1:MarkerOptions = new MarkerOptions();
            var markerOptions2:MarkerOptions = new MarkerOptions();

            var angle:Number = bearing(lineOut.getVertex(Math.round(lineOutLength/2)-1),lineOut.getVertex(Math.round(lineOutLength/2)-0));
            markerOptions1.icon = new CustomArrow(angle,0,0);
            angle = bearing(lineIn.getVertex(Math.round(lineInLength/2)-1),lineIn.getVertex(Math.round(lineInLength/2)-0));
            markerOptions2.icon = new CustomArrow(angle,0,0);

            markerOptions1.iconAlignment = MarkerOptions.ALIGN_TOP;
            markerOptions2.iconAlignment = MarkerOptions.ALIGN_TOP;

            var marker1:Marker = new Marker(point1, markerOptions1);
            var marker2:Marker = new Marker(point2, markerOptions2);
            arrowsOnMap[name+"_out"]=marker1;
            arrowsOnMap[name+"_in"]=marker2;

            linksOnMap[name+"_in"]=lineIn;
            linksOnMap[name+"_out"]=lineOut;
        }

        /*
         * Find if source and destination cross the international date line, and
         * calculates offset for from and to so all in one hemisphere.  Probably a
         * more elegant solution.  Actionscript version of Javascript.
         */
        private function getLngOffset(lngFrom:Number, lngTo:Number):Number {
            var offset:Number = 0.0;
            var fromDist:Number = 180.0 - Math.abs(lngFrom);
            var toDist:Number  = 180.0 - Math.abs(lngTo);
            if ((fromDist + toDist) < 180.0) {
                if ((lngFrom < 0.0) && (lngTo > 0.0)) {
                    offset = 181.0 - lngTo;
                } else if ((lngTo < 0.0) && (lngFrom > 0.0)) {
                    offset = 181.0 - lngFrom;
                }
            }
            return offset;
        }

        /*
         * Adds to a longitude, converting to other hemisphere if necessary.
         */
        private function adjustLngTo(lng:Number, offset:Number):Number {
            var newLng:Number = lng;
            if (offset != 0.0) {
                newLng += offset;
                if (newLng > 180.0) {
                    newLng = -180.0 + (newLng - 180.0);
                }
            }
            return newLng;
        }

        /*
         * Subtracts from a longitude, converting to other hemisphere if necessary.
         */
        private function adjustLngFrom(lng:Number, offset:Number):Number {
            var newLng:Number = lng;
            if (offset != 0.0) {
                newLng -= offset;
                if (newLng < -180.0) {
                    newLng = newLng + 360;
                }
            }
            return newLng;
        }

        private function createMapCurve(topoint:LatLng, from:LatLng, index:int,
                                        segment:String):Array {
            var seg:String = segment || "whole";
            var p0:Vector2D = new Vector2D(topoint.lng(), topoint.lat());
            var p3:Vector2D = new Vector2D(from.lng(), from.lat());

            var angle:Number = Math.PI * (1.5 + .45 * index) / 36;
            var magnitudeScale:Number = (1.5 + .35 * index) / 2.6;
            var controlPoints:Array = Bezier.getControlPoints(p0, p3, magnitudeScale, angle);
            var c1:Vector2D = controlPoints[0];
            var c2:Vector2D = controlPoints[1];
            var curveXY:Array = Bezier.getBezier(p0, c1, c2, p3, 5, seg);
            var curveLL:Array = new Array();
            for(var i:int = 0; i < curveXY.length; i++){
                curveLL.push(new LatLng(curveXY[i].gety(), curveXY[i].getx()));
            }
            return curveLL;
        }

        public function bearing(fromLL:LatLng, toLL:LatLng):Number {
            var degreesPerRadian:Number = 180.0 / Math.PI;
            var currentProjection:IProjection = map.getProjection();
            var highestZoom:Number = map.getZoom();
            // The second parameter is the zoom level (get it from the highest zoom level)
            var frompoint:Point = map.fromLatLngToPoint(fromLL, highestZoom);
            var topoint:Point = map.fromLatLngToPoint(toLL, highestZoom);

            var x:Number = topoint.x - frompoint.x;
            var y:Number = topoint.y - frompoint.y;
            if ((x == 0) && (y == 0)) {
               return 90;  // arbitrary: to and from are at same point
            }
            var mag:Number = Math.sqrt(x*x + y*y);

            // Compute the angle.
            var angle:Number = Math.acos(-y/mag);
            if ( x < 0.0)
                angle  = Math.PI * 2.0 - angle;

            // And convert result to degrees.
            angle = angle * degreesPerRadian;

            return angle;
        }


        public function createPathDS(nodeLat:Array, nodeLong:Array,
                      numOfHops:Number,bandwidth:Number, pathname:String):void {
           var coord:Array = new Array(numOfHops);
           var num:Number = numOfHops;
           for (var i:Number=0;i<num;i++){
                coord[i] = new LatLng(nodeLat[i],nodeLong[i]);
                if(num == 1){
                    coord = new Array(numOfHops+3);
                    coord[0] = new LatLng(nodeLat[0],nodeLong[0]);
                    coord[1] = new LatLng(nodeLat[1],nodeLong[1]);
                    coord[2] = new LatLng(nodeLat[2],nodeLong[2]);
                    coord[3] = new LatLng(nodeLat[3],nodeLong[3]);
                }
           }

           if (coord != null) {
               pathToAdd = new Polyline(coord);
               var width:Number = getOSCARSLineWidth(bandwidth);
               var pathColor:Number = 0x660099;
               var options:PolylineOptions = new PolylineOptions( { strokeStyle: { thickness: width, color: pathColor, alpha: 1 }} );
               pathToAdd.setOptions(options);
               pathName=pathname;
               var pathNameCoord:LatLng = coord[Math.round(numOfHops/2)];
           }
        }

        public function drawPath():void {
            if (pathToRemove != null) {
                map.removeOverlay(pathToRemove);
                pathToRemove = undefined;
            }
            if (pathToAdd != null) {
                map.addOverlay(pathToAdd);
                pathOnMap = pathToAdd;
                pathToRemove = pathToAdd;
                pathToAdd = undefined;
            }
        }

        public function setLabelVisibility(showLabel:Boolean):void {
            var options:MarkerOptions;
            if (showLabel==true){
                for(var marker:String in markersOnMap) {
                    options = new MarkerOptions({icon: new CustomIconSprite(marker,typeOfRouter[marker])});
                    markersOnMap[marker].setOptions(options);
                }

            } else {
                for(var markerA:String in markersOnMap){
                    options = new MarkerOptions({icon: new CustomIconSprite("",typeOfRouter[markerA])});
                    markersOnMap[markerA].setOptions(options);
                }
            }
        }


        public function clearMap():void {
            map.clearOverlays();
            markersOnMap = new Array();
            linksOnMap = new Array();
            prevColorsOnMap = new Array();
            typeOfRouter = new Array();
            markerLocation = new Array();
        }

        public function setMapBG(lvVal:int):void {
            if (bg != null) {
                map.removeOverlay(bg);
            }
            var viewport:LatLngBounds = map.getLatLngBounds();
            if (lvVal == 1) {
                bg = new GroundOverlay(new CustomIconSprite("","whitebg"),
                                       viewport);
                map.addOverlay(bg);
            }
            var northEast:LatLng = viewport.getNorthEast();
            var point:Point = map.fromLatLngToViewport(northEast,true);
            var newPoint:Point = new Point(point.x-150,point.y-5);
            if (markerLogo != null) {
                map.removeOverlay(markerLogo);
            }
            var markerCoord:LatLng = map.fromViewportToLatLng(newPoint);
            markerLogo = new Marker(markerCoord,
                                    new MarkerOptions({
                                    icon: new CustomIconSprite("(c)Copyright ESnet 2009","logo"),
                                    hasShadow:false
                         }));
            map.addOverlay(markerLogo);
        }

        public function updateInfoCard(table:Array):void {
            if (!infoWindowClosed && ExternalInterface.available) {
                //infoWindowOptions.customContent = new InfoWindowTabbedComponent(table);
            }
        }

        public function updateLinksOnMap(connName:String, capacity:Number,
                              utilization:Number, utilizationType:String):void {
            if (linksOnMap[connName] == null) {
                return;
            }
            var lineColor:Number = getLineColor(utilization, utilizationType);
            if ((prevColorsOnMap[connName] != null) &&
                (prevColorsOnMap[connName] == lineColor)) {
                return;
            }
            prevColorsOnMap[connName] = lineColor;
            var width:Number = getLineWidth(capacity);
            var options:PolylineOptions = new PolylineOptions(
                { strokeStyle: { thickness: width, color: lineColor, alpha: 1 }} );
            linksOnMap[connName].setOptions(options);
        }

        private function getLineWidth(cap:Number):Number {
            var width:Number = 1;
            if (cap>0 && cap<999) {
                width=1;
            } else if (cap>999 && cap<9999) {
                width=1.5;
            } else if (cap>9999 && cap<19999) {
                width=2;
            } else if (cap>19999 && cap<39999) {
                width=2.5;
            } else {
                width=3;
            }
            return width;
        }

        private function getLineColor(utilization:Number,
                                      utilizationType:String):Number {
            if (utilizationType == "percent") {
                if (utilization < 0.0) {
                    return 0x444444;
                } else if (utilization < 0.05) {
                    return 0x0066CC;
                } else if (utilization < 0.10) {
                    return 0x009900;
                } else if (utilization < 0.20) {
                    return 0x99FF00;
                } else if (utilization < 0.30) {
                    return 0xFFFF00;
                } else if (utilization < 0.40) {
                    return 0xFF9933;
                } else if (utilization < 0.50) {
                    return 0xCC3333;
                } else {
                    return 0x990033;
                }
            } else {
                if (utilization < 100) {
                    return 0x0066CC;
                } else if (utilization < 250) {
                    return 0x009900;
                } else if (utilization < 500) {
                    return 0x99FF00;
                } else if (utilization < 1000) {
                    return 0xFFFF00;
                } else if (utilization < 5000) {
                    return 0xFF9933;
                } else if (utilization < 10000) {
                    return 0xCC3333;
                } else {
                    return 0x990033;
                }
            }
        }

        private function getOSCARSLineWidth(cap:Number):Number {
            var width:Number = 1;
            if (cap>0 && cap<=500) {
                width=5;
            } else if (cap>500 && cap<=1000) {
                width =8;
            } else if (cap>1000 && cap<=5000) {
                width=12;
            } else {
                width =14;
            }
            return width;
        }

        public function getCoord(dir:String):Number {
            var latlng:LatLngBounds = map.getLatLngBounds();
            var coord:Number;
            if (dir=="N") {
                coord = latlng.getNorth();
            } else if (dir=="S") {
                coord = latlng.getSouth();
            } else if (dir=="E") {
                coord = latlng.getEast();
            } else if (dir =="W") {
                coord = latlng.getWest();
            } else {
                coord = 0;
            }
            return coord;
        }

        public function getZoom():Number {
            return map.getZoom();
        }

        public function getMarkersInViewPort(viewport:LatLngBounds):Array {
            var markersInView:Array = new Array();
            if (markerLocation == null) {
                return markersInView;
            }
            for (var marker:String in markerLocation) {
                if (viewport.containsLatLng(markerLocation[marker])) {
                    markersInView.push(marker);
                }
            }
            return markersInView;
        }
    } // class end

} // package end
